package com.guosen.zebra.framework.starter.http.log.utils;

import com.guosen.zebra.framework.starter.http.log.vo.HttpAccLogVO;
import com.guosen.zebra.framework.starter.http.log.vo.HttpBizReqLogVO;
import com.guosen.zebra.framework.starter.http.log.vo.HttpBizRspLogVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import static com.guosen.zebra.framework.starter.http.log.constants.HttpLogConstant.*;
import static org.springframework.web.servlet.HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;

/**
 * 接口日志打印helper类
 */
@Slf4j
public class HttpLogHelper {

    private static final Logger ACC_LOGGER = LoggerFactory.getLogger("zebra.http.acc");
    private static final Logger BIZ_LOGGER = LoggerFactory.getLogger("zebra.http.biz");
    private HttpLogHelper() {}

    public static void logAcc(ContentCachingRequestWrapper req, ContentCachingResponseWrapper rsp) {
        try {
            HttpAccLogVO accLogVO = buildZebraHttpAccLogVO(req, rsp);
            String logContent = JsonParser.toJSONString(accLogVO);
            ACC_LOGGER.info(logContent);
        } catch (Exception e) {
            log.error("Failed to log acc, error message:{}", e.getMessage(), e);
        }
    }

    public static void logBizRequest(ContentCachingRequestWrapper req) {
        try {
            HttpBizReqLogVO bizReqLogVO = buildZebraHttpBizReqLogVO(req);
            String content = JsonParser.toJSONString(bizReqLogVO);
            BIZ_LOGGER.info(content);
        } catch (Exception e) {
            log.error("Failed to log biz request, error message:{}", e.getMessage(), e);
        }
    }

    public static void logBizResponse(ContentCachingRequestWrapper req, ContentCachingResponseWrapper rsp) {
        try {
            HttpBizRspLogVO bizRspLogVO = buildZebraHttpBizRspLogVO(req, rsp);
            String content = JsonParser.toJSONString(bizRspLogVO);
            BIZ_LOGGER.info(content);
        } catch (Exception e) {
            log.error("Failed to log biz response, error message:{}", e.getMessage(), e);
        }
    }

    private static HttpBizReqLogVO buildZebraHttpBizReqLogVO(ContentCachingRequestWrapper req) {
        long reqTime = (Long) req.getAttribute(ZEBRA_HTTP_LOG_START_TIME);
        HttpBizReqLogVO bizReqLogVO = new HttpBizReqLogVO();
        bizReqLogVO.setT(TimeUtil.getFormatStrFromMillis(reqTime));
        bizReqLogVO.setMethod(req.getMethod());
        bizReqLogVO.setContentType(req.getContentType());
        bizReqLogVO.setPath(req.getRequestURI());
        bizReqLogVO.setTraceId(TraceContext.traceId());
        bizReqLogVO.setReqParams(buildReqParams(req));
        bizReqLogVO.setServerAddr(IpUtil.getServerAddr());
        bizReqLogVO.setRemoteAddr(IpUtil.getIp(req));
        return bizReqLogVO;
    }

    private static HttpAccLogVO buildZebraHttpAccLogVO(ContentCachingRequestWrapper req, ContentCachingResponseWrapper rsp) {
        long reqTime = (Long) req.getAttribute(ZEBRA_HTTP_LOG_START_TIME);
        long rspTime = (Long) req.getAttribute(ZEBRA_HTTP_LOG_END_TIME);
        HttpAccLogVO accLogVO = new HttpAccLogVO();
        accLogVO.setTraceId(TraceContext.traceId());
        accLogVO.setMethod(req.getMethod());
        accLogVO.setPath(req.getRequestURI());
        accLogVO.setHttpStatusCode(String.valueOf(rsp.getStatus()));
        accLogVO.setReqTime(TimeUtil.getFormatStrFromMillis(reqTime));
        accLogVO.setRspTime(TimeUtil.getFormatStrFromMillis(rspTime));
        accLogVO.setProcessTime(String.valueOf(rspTime - reqTime));
        accLogVO.setServerAddr(IpUtil.getServerAddr());
        accLogVO.setRemoteAddr(IpUtil.getIp(req));
        return accLogVO;
    }

    private static HttpBizRspLogVO buildZebraHttpBizRspLogVO(ContentCachingRequestWrapper req, ContentCachingResponseWrapper rsp) {
        long rspTime = (Long) req.getAttribute(ZEBRA_HTTP_LOG_END_TIME);
        HttpBizRspLogVO bizRspLogVO = new HttpBizRspLogVO();
        bizRspLogVO.setT(TimeUtil.getFormatStrFromMillis(rspTime));
        bizRspLogVO.setTraceId(TraceContext.traceId());
        bizRspLogVO.setMethod(req.getMethod());
        bizRspLogVO.setPath(req.getRequestURI());
        bizRspLogVO.setContentType(rsp.getContentType());
        bizRspLogVO.setHttpStatusCode(String.valueOf(rsp.getStatus()));
        bizRspLogVO.setRsp(getResponseContent(rsp));
        bizRspLogVO.setServerAddr(IpUtil.getServerAddr());
        bizRspLogVO.setRemoteAddr(IpUtil.getIp(req));
        return bizRspLogVO;
    }

    private static HttpBizReqLogVO.ReqParams buildReqParams(ContentCachingRequestWrapper req) {
        HttpBizReqLogVO.ReqParams reqParams = new HttpBizReqLogVO.ReqParams();
        reqParams.setPath(getPathParams(req));
        reqParams.setQuery(getQueryParams(req));
        reqParams.setBody(getRequestContent(req));
        return reqParams;
    }

    private static Map<String, Object> getPathParams(ContentCachingRequestWrapper request) {
        Map<String, Object> pathParams = (Map<String, Object>) request.getAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE);
        return CollectionUtils.isEmpty(pathParams) ? new HashMap<>() : pathParams;
    }

    private static Map<String, Object> getQueryParams(ContentCachingRequestWrapper req) {
        Map<String, Object> queryParams = new HashMap<>();
        String queryString = req.getQueryString();
        if (!StringUtils.hasLength(queryString))
            return queryParams;
        String[] pairs = queryString.split("&");
        try {
            for (String pair : pairs) {
                String[] kv = pair.split("=");
                String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8.name());
                String value = kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8.name()) : "";
                queryParams.put(key, value);
            }
        } catch (UnsupportedEncodingException e) {
            // 实际不会发生
            log.error("unexpected error happened. error message:{}", e.getMessage(), e);
        }
        return queryParams;
    }

    private static String getRequestContent(ContentCachingRequestWrapper req) {
        return checkIfLog(req.getContentType()) ?
                getContentAsString(req.getContentAsByteArray()) : null;
    }

    private static String getResponseContent(ContentCachingResponseWrapper rsp) {
        return checkIfLog(rsp.getContentType()) ?
                getContentAsString(rsp.getContentAsByteArray()) : null;
    }

    private static String getContentAsString(byte[] buf) {
        if (buf == null || buf.length == 0) return "";
        int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);
        return new String(buf, 0, length, StandardCharsets.UTF_8);
    }

    private static boolean checkIfLog(String contentType) {
        if (!StringUtils.hasLength(contentType)) {
            return true;
        }
        return CONTENT_TYPE_IGNORE_LIST.stream()
                .noneMatch(v -> contentType.toLowerCase().contains(v));
    }
}
